package com.springframework.session.web.http;

import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.springframework.session.Session;
import com.springframework.session.SessionRepository;

/**
 * A {@link javax.servlet.http.HttpServletRequest} that retrieves the
 * {@link javax.servlet.http.HttpSession} using a
 * {@link com.springframework.session.SessionRepository}.
 *
 * @author Rob Winch
 * @since 1.0
 */
final class SessionRepositoryRequestWrapper<S extends Session> extends HttpServletRequestWrapper {
	private static final String SESSION_LOGGER_NAME = SessionRepositoryRequestWrapper.class.getName()
			.concat(".SESSION_LOGGER");

	private static final Logger SESSION_LOGGER = LoggerFactory.getLogger(SESSION_LOGGER_NAME);
	private final String CURRENT_SESSION_ATTR = HttpServletRequestWrapper.class.getName();
	private Boolean requestedSessionIdValid;
	private boolean requestedSessionInvalidated;
	private final HttpServletResponse response;
	private final ServletContext servletContext;
	private HttpSessionStrategy httpSessionStrategy;
	private  SessionRepository<S> sessionRepository=null;
	/**
	 * The session repository request attribute name.
	 */
	public static final String SESSION_REPOSITORY_ATTR = SessionRepositoryRequestWrapper.class.getName();
	/**
	 * Invalid session id (not backed by the session repository) request attribute
	 * name.
	 */
	public static final String INVALID_SESSION_ID_ATTR = SESSION_REPOSITORY_ATTR.concat(".invalidSessionId");

	SessionRepositoryRequestWrapper(HttpServletRequest request, HttpServletResponse response,
			ServletContext servletContext, SessionRepository<S> sessionRepository,
			HttpSessionStrategy httpSessionStrategy) {
		super(request);
		request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);
		this.response = response;
		this.servletContext = servletContext;
		this.httpSessionStrategy = httpSessionStrategy;
		this.sessionRepository = sessionRepository;
	}

	/**
	 * Uses the HttpSessionStrategy to write the session id to the response and
	 * persist the Session.
	 */
	void commitSession() {
		HttpSessionWrapper wrappedSession = (HttpSessionWrapper) getAttribute(this.CURRENT_SESSION_ATTR);
		if (wrappedSession == null) {
			if (isInvalidateClientSession()) {
				httpSessionStrategy.onInvalidateSession(this, this.response);
			}
		} else {
			S session = wrappedSession.getSession();
			sessionRepository.save(session);
			if (!isRequestedSessionIdValid() || !session.getId().equals(getRequestedSessionId())) {
				httpSessionStrategy.onNewSession(session, this, this.response);
			}
		}
	}

	private void setCurrentSession(HttpSessionWrapper currentSession) {
		if (currentSession == null) {
			removeAttribute(this.CURRENT_SESSION_ATTR);
		} else {
			setAttribute(this.CURRENT_SESSION_ATTR, currentSession);
		}
	}

	public String changeSessionId() {
		HttpSession session = getSession(false);

		if (session == null) {
			throw new IllegalStateException(
					"Cannot change session ID. There is no session associated with this request.");
		}

		// eagerly get session attributes in case implementation lazily loads them
		Map<String, Object> attrs = new HashMap<String, Object>();
		Enumeration<String> iAttrNames = session.getAttributeNames();
		while (iAttrNames.hasMoreElements()) {
			String attrName = iAttrNames.nextElement();
			Object value = session.getAttribute(attrName);
			attrs.put(attrName, value);
		}

		sessionRepository.delete(session.getId());
		HttpSessionWrapper original = (HttpSessionWrapper) getAttribute(CURRENT_SESSION_ATTR);
		setCurrentSession(null);

		HttpSessionWrapper newSession = getSession();
		original.setSession(newSession.getSession());

		newSession.setMaxInactiveInterval(session.getMaxInactiveInterval());
		for (Map.Entry<String, Object> attr : attrs.entrySet()) {
			String attrName = attr.getKey();
			Object attrValue = attr.getValue();
			newSession.setAttribute(attrName, attrValue);
		}
		return newSession.getId();
	}

	@Override
	public boolean isRequestedSessionIdValid() {
		if (this.requestedSessionIdValid == null) {
			String sessionId = getRequestedSessionId();
			S session = sessionId == null ? null : getSession(sessionId);
			return isRequestedSessionIdValid(session);
		}

		return this.requestedSessionIdValid;
	}

	private boolean isRequestedSessionIdValid(S session) {
		if (this.requestedSessionIdValid == null) {
			this.requestedSessionIdValid = session != null;
		}
		return this.requestedSessionIdValid;
	}

	private boolean isInvalidateClientSession() {
		return getAttribute(this.CURRENT_SESSION_ATTR) == null && this.requestedSessionInvalidated;
	}

	private S getSession(String sessionId) {
		S session = sessionRepository.getSession(sessionId);
		if (session == null) {
			return null;
		}
		session.setLastAccessedTime(System.currentTimeMillis());
		return session;
	}

	@Override
	public HttpSessionWrapper getSession(boolean create) {
		HttpSessionWrapper currentSession = (HttpSessionWrapper) getAttribute(this.CURRENT_SESSION_ATTR);
		if (currentSession != null) {
			return currentSession;
		}
		String requestedSessionId = getRequestedSessionId();
		if (requestedSessionId != null && getAttribute(INVALID_SESSION_ID_ATTR) == null) {
			S session = getSession(requestedSessionId);
			if (session != null) {
				this.requestedSessionIdValid = true;
				currentSession = new HttpSessionWrapper(session, getServletContext());
				currentSession.setNew(false);
				setCurrentSession(currentSession);
				return currentSession;
			} else {
				// This is an invalid session id. No need to ask again if
				// request.getSession is invoked for the duration of this request
				if (SESSION_LOGGER.isDebugEnabled()) {
					SESSION_LOGGER.debug(
							"No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
				}
				setAttribute(INVALID_SESSION_ID_ATTR, "true");
			}
		}
		if (!create) {
			return null;
		}
		S session = sessionRepository.createSession();
		session.setLastAccessedTime(System.currentTimeMillis());
		currentSession = new HttpSessionWrapper(session, getServletContext());
		setCurrentSession(currentSession);
		return currentSession;
	}

	public ServletContext getServletContext() {
		if (this.servletContext != null) {
			return this.servletContext;
		}
		// Servlet 3.0+
		return null;
	}

	@Override
	public HttpSessionWrapper getSession() {
		return getSession(true);
	}

	@Override
	public String getRequestedSessionId() {
		return httpSessionStrategy.getRequestedSessionId(this);
	}

	/**
	 * Allows creating an HttpSession from a Session instance.
	 *
	 * @author Rob Winch
	 * @since 1.0
	 */
	private final class HttpSessionWrapper extends ExpiringSessionHttpSession<S> {

		HttpSessionWrapper(S session, ServletContext servletContext) {
			super(session, servletContext);
		}

		@Override
		public void invalidate() {
			super.invalidate();
			requestedSessionInvalidated = true;
			setCurrentSession(null);
			sessionRepository.delete(getId());
		}
	}
}